-- @docclass
g_keyboard = {}

-- private functions
function translateKeyCombo(keyCombo)
    if not keyCombo or #keyCombo == 0 then
        return nil
    end
    local keyComboDesc = ''
    for k, v in pairs(keyCombo) do
        local keyDesc = KeyCodeDescs[v]
        if keyDesc == nil then
            return nil
        end
        keyComboDesc = keyComboDesc .. '+' .. keyDesc
    end
    keyComboDesc = keyComboDesc:sub(2)
    return keyComboDesc
end

local function getKeyCode(key)
    for keyCode, keyDesc in pairs(KeyCodeDescs) do
        if keyDesc:lower() == key:trim():lower() then
            return keyCode
        end
    end
end

function retranslateKeyComboDesc(keyComboDesc)
    if keyComboDesc == nil then
        error('Unable to translate key combo \'' .. keyComboDesc .. '\'')
    end

    if type(keyComboDesc) == 'number' then
        keyComboDesc = tostring(keyComboDesc)
    end

    local keyCombo = {}
    for i, currentKeyDesc in ipairs(keyComboDesc:split('+')) do
        for keyCode, keyDesc in pairs(KeyCodeDescs) do
            if keyDesc:lower() == currentKeyDesc:trim():lower() then
                table.insert(keyCombo, keyCode)
            end
        end
    end
    return translateKeyCombo(keyCombo)
end

function determineKeyComboDesc(keyCode, keyboardModifiers)
    local keyCombo = {}
    if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then
        table.insert(keyCombo, keyCode)
    elseif KeyCodeDescs[keyCode] ~= nil then
        if keyboardModifiers == KeyboardCtrlModifier then
            table.insert(keyCombo, KeyCtrl)
        elseif keyboardModifiers == KeyboardAltModifier then
            table.insert(keyCombo, KeyAlt)
        elseif keyboardModifiers == KeyboardCtrlAltModifier then
            table.insert(keyCombo, KeyCtrl)
            table.insert(keyCombo, KeyAlt)
        elseif keyboardModifiers == KeyboardShiftModifier then
            table.insert(keyCombo, KeyShift)
        elseif keyboardModifiers == KeyboardCtrlShiftModifier then
            table.insert(keyCombo, KeyCtrl)
            table.insert(keyCombo, KeyShift)
        elseif keyboardModifiers == KeyboardAltShiftModifier then
            table.insert(keyCombo, KeyAlt)
            table.insert(keyCombo, KeyShift)
        elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then
            table.insert(keyCombo, KeyCtrl)
            table.insert(keyCombo, KeyAlt)
            table.insert(keyCombo, KeyShift)
        end
        table.insert(keyCombo, keyCode)
    end
    return translateKeyCombo(keyCombo)
end

local function onWidgetKeyDown(widget, keyCode, keyboardModifiers)
    if keyCode == KeyUnknown then
        return false
    end
    local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
    signalcall(callback, widget, keyCode)
    callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
    return signalcall(callback, widget, keyCode)
end

local function onWidgetKeyUp(widget, keyCode, keyboardModifiers)
    if keyCode == KeyUnknown then
        return false
    end
    local callback = widget.boundAloneKeyUpCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
    signalcall(callback, widget, keyCode)
    callback = widget.boundKeyUpCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
    return signalcall(callback, widget, keyCode)
end

local function onWidgetKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks)
    if keyCode == KeyUnknown then
        return false
    end
    local callback = widget.boundKeyPressCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
    return signalcall(callback, widget, keyCode, autoRepeatTicks)
end

local function connectKeyDownEvent(widget)
    if widget.boundKeyDownCombos then
        return
    end
    connect(widget, {
        onKeyDown = onWidgetKeyDown
    })
    widget.boundKeyDownCombos = {}
    widget.boundAloneKeyDownCombos = {}
end

local function connectKeyUpEvent(widget)
    if widget.boundKeyUpCombos then
        return
    end
    connect(widget, {
        onKeyUp = onWidgetKeyUp
    })
    widget.boundKeyUpCombos = {}
    widget.boundAloneKeyUpCombos = {}
end

local function connectKeyPressEvent(widget)
    if widget.boundKeyPressCombos then
        return
    end
    connect(widget, {
        onKeyPress = onWidgetKeyPress
    })
    widget.boundKeyPressCombos = {}
end

-- public functions
function g_keyboard.bindKeyDown(keyComboDesc, callback, widget, alone)
    widget = widget or rootWidget
    connectKeyDownEvent(widget)
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    if alone then
        connect(widget.boundAloneKeyDownCombos, keyComboDesc, callback)
    else
        connect(widget.boundKeyDownCombos, keyComboDesc, callback)
    end
end

function g_keyboard.bindKeyUp(keyComboDesc, callback, widget, alone)
    widget = widget or rootWidget
    connectKeyUpEvent(widget)
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    if alone then
        connect(widget.boundAloneKeyUpCombos, keyComboDesc, callback)
    else
        connect(widget.boundKeyUpCombos, keyComboDesc, callback)
    end
end

function g_keyboard.bindKeyPress(keyComboDesc, callback, widget)
    widget = widget or rootWidget
    connectKeyPressEvent(widget)
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    connect(widget.boundKeyPressCombos, keyComboDesc, callback)
end

local function getUnbindArgs(arg1, arg2)
    local callback
    local widget
    if type(arg1) == 'function' then
        callback = arg1
    elseif type(arg2) == 'function' then
        callback = arg2
    end
    if type(arg1) == 'userdata' then
        widget = arg1
    elseif type(arg2) == 'userdata' then
        widget = arg2
    end
    widget = widget or rootWidget
    return callback, widget
end

function g_keyboard.unbindKeyDown(keyComboDesc, arg1, arg2)
    local callback, widget = getUnbindArgs(arg1, arg2)
    if widget.boundKeyDownCombos == nil then
        return
    end
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    disconnect(widget.boundKeyDownCombos, keyComboDesc, callback)
end

function g_keyboard.unbindKeyUp(keyComboDesc, arg1, arg2)
    local callback, widget = getUnbindArgs(arg1, arg2)
    if widget.boundKeyUpCombos == nil then
        return
    end
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    disconnect(widget.boundKeyUpCombos, keyComboDesc, callback)
end

function g_keyboard.unbindKeyPress(keyComboDesc, arg1, arg2)
    local callback, widget = getUnbindArgs(arg1, arg2)
    if widget.boundKeyPressCombos == nil then
        return
    end
    local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
    disconnect(widget.boundKeyPressCombos, keyComboDesc, callback)
end

function g_keyboard.getModifiers()
    return g_window.getKeyboardModifiers()
end

function g_keyboard.isKeyPressed(key)
    if type(key) == 'string' then
        key = getKeyCode(key)
    end
    return g_window.isKeyPressed(key)
end

function g_keyboard.isKeySetPressed(keys, all)
    all = all or false
    local result = {}
    for k, v in pairs(keys) do
        if type(v) == 'string' then
            v = getKeyCode(v)
        end
        if g_window.isKeyPressed(v) then
            if not all then
                return true
            end
            table.insert(result, true)
        end
    end
    return #result == #keys
end

function g_keyboard.isInUse()
    for i = FirstKey, LastKey do
        if g_window.isKeyPressed(key) then
            return true
        end
    end
    return false
end

function g_keyboard.isCtrlPressed()
    if (g_platform.isMobile()) then
        return false
    else
        return bit.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0
    end
end

function g_keyboard.isAltPressed()
    if (g_platform.isMobile()) then
        return false
    else
        return bit.band(g_window.getKeyboardModifiers(), KeyboardAltModifier) ~= 0
    end
end

function g_keyboard.isShiftPressed()
    if (g_platform.isMobile()) then
        return false
    else
        return bit.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0
    end
end
